package database

import (
	"abdis/interface/database"
	"abdis/interface/datastruct"
	"abdis/interface/redis"
	"abdis/lib/utils"
	"abdis/lib/wildcard"
	"abdis/redis/protocol"
	"math"
	"strconv"
	"strings"
	"time"
)

func toTTLCmd(db *DB, key string) *protocol.MultiBulkReply {
	raw, exists := db.ttlMap.Get(key)
	if !exists {
		return protocol.MakeMultiBulkReply(utils.ToCmdLine("PERSIST", key))
	}
	expireTime, _ := raw.(time.Time)
	timestamp := strconv.FormatInt(expireTime.UnixNano()/1000/1000, 10)
	return protocol.MakeMultiBulkReply(utils.ToCmdLine("PEXPIREAT", key, timestamp))
}

func execDel(db *DB, args [][]byte) (redis.Reply, bool) {
	keys := utils.BytesToStrings(args)

	deleted := db.Removes(keys...)

	return protocol.MakeIntReply(int64(deleted)), true
}

func undoDel(db *DB, args [][]byte) []database.CmdLine {
	return rollbackGivenKeys(db, utils.BytesToStrings(args)...)
}

func execTTL(db *DB, args [][]byte) (redis.Reply, bool) {
	key := string(args[0])
	_, exists := db.GetEntity(key)
	if !exists {
		return protocol.MakeIntReply(-2), false
	}

	raw, exists := db.ttlMap.Get(key)
	if !exists {
		return protocol.MakeIntReply(-1), false
	}
	expireTime, _ := raw.(time.Time)
	ttl := expireTime.Sub(time.Now()).Seconds()
	return protocol.MakeIntReply(int64(math.Round(ttl))), true
}

// execPersist 使得一个键变为永久存储
func execPersist(db *DB, args [][]byte) (redis.Reply, bool) {
	key := string(args[0])
	_, exists := db.GetEntity(key)
	if !exists {
		return protocol.MakeIntReply(0), false
	}

	_, exists = db.ttlMap.Get(key)
	if !exists {
		return protocol.MakeIntReply(0), false
	}

	db.Persist(key)
	return protocol.MakeIntReply(1), true
}

func execPExpireAt(db *DB, args [][]byte) (redis.Reply, bool) {
	key := string(args[0])
	raw, err := strconv.ParseInt(string(args[1]), 10, 64)
	if err != nil {
		return protocol.MakeErrReply("ERR value is not an integer"), false
	}

	expireAt := time.Unix(0, raw*int64(time.Millisecond))
	_, exists := db.GetEntity(key)
	if !exists {
		return protocol.MakeIntReply(0), false
	}

	db.Expire(key, expireAt)

	db.addAof(utils.ToExpireCmd(key, &expireAt))

	return protocol.MakeIntReply(1), true
}

func execKeys(db *DB, args [][]byte) (redis.Reply, bool) {
	pattern, err := wildcard.CompilePattern(string(args[0]))
	if err != nil {
		return protocol.MakeErrReply("ERR illegal wildcard"), false
	}
	result := make([][]byte, 0)
	db.data.ForEach(func(key string, val any) bool {
		if pattern.IsMatch(key) && !db.IsExpired(key) {
			result = append(result, []byte(key))
		}
		return true
	})
	return protocol.MakeMultiBulkReply(result), true
}

func execType(db *DB, args [][]byte) (redis.Reply, bool) {
	key := string(args[0])
	result := getType(db, key)
	if len(result) > 0 {
		return protocol.MakeStatusReply(result), true
	} else {
		return protocol.MakeUnknownErrReply(), false
	}
}

func getType(db *DB, key string) string {
	entity, exists := db.GetEntity(key)
	if !exists {
		return "none"
	}
	switch entity.Data.(type) {
	case []byte:
		return "string"
	case datastruct.List:
		return "list"
	case datastruct.Dict:
		return "hash"
	case datastruct.Set:
		return "set"
	// TODO
	default:
		return ""
	}
}

func undoExpire(db *DB, args [][]byte) []database.CmdLine {
	key := string(args[0])
	return []database.CmdLine{
		toTTLCmd(db, key).Args,
	}
}

func execExpire(db *DB, args [][]byte) (redis.Reply, bool) {
	key := string(args[0])
	ttlArg, err := strconv.ParseInt(string(args[1]), 10, 64)
	if err != nil {
		return protocol.MakeErrReply("ERR value is invalid"), false
	}

	ttl := time.Duration(ttlArg) * time.Second

	_, exists := db.GetEntity(key)
	if !exists {
		return protocol.MakeIntReply(0), false
	}

	t := time.Now().Add(ttl)
	db.Expire(key, t)

	db.addAof(utils.ToExpireCmd(key, &t))

	return protocol.MakeIntReply(1), true
}

func execExists(db *DB, args [][]byte) (redis.Reply, bool) {
	result := int64(0)
	for _, arg := range args {
		_, exists := db.GetEntity(string(arg))
		if exists {
			result++
		}
	}
	return protocol.MakeIntReply(result), true
}

func execScan(db *DB, args [][]byte) (redis.Reply, bool) {
	// logger.Info(utils.BytesToStrings(args))
	count := 10
	pattern := "*"
	scanType := ""
	if len(args) > 1 {
		for i := 1; i < len(args); i++ {
			arg := strings.ToLower(string(args[i]))
			switch arg {
			case "count":
				num, err := strconv.Atoi(string(args[i+1]))
				if err != nil {
					return protocol.MakeSyntaxErrReply(), false
				}
				count = num
				i++
			case "match":
				pattern = string(args[i+1])
				i++
			case "type":
				scanType = strings.ToLower(string(args[i+1]))
				i++
			default:
				return protocol.MakeSyntaxErrReply(), false
			}
		}
	}

	cursor, err := strconv.Atoi(string(args[0]))
	if err != nil {
		return protocol.MakeErrReply("ERR invalid cursor"), false
	}

	keys, nextCursor := db.data.Scan(cursor, count, pattern)
	if nextCursor < 0 {
		return protocol.MakeErrReply("ERR invalid argument"), false
	}

	if len(scanType) != 0 {
		for i := 0; i < len(keys); {
			if getType(db, string(keys[i])) != scanType {
				keys = append(keys[:i], keys[i+1:]...)
			} else {
				i++
			}
		}
	}

	result := make([]redis.Reply, 2)
	result[0] = protocol.MakeBulkReply([]byte(strconv.FormatInt(int64(nextCursor), 10)))
	result[1] = protocol.MakeMultiBulkReply(keys)

	return protocol.MakeMultiRawReply(result), true
}

func init() {
	registerCommand("Del", execDel, writeAllKeys, undoDel, -2, flagWrite)

	registerCommand("Expire", execExpire, writeFirstKey, undoExpire, 3, flagWrite|flagExpire)

	registerCommand("PExpireAt", execPExpireAt, writeFirstKey, undoExpire, 3, flagWrite|flagExpire)

	registerCommand("Persist", execPersist, writeFirstKey, undoExpire, 2, flagWrite)

	registerCommand("Exists", execExists, readAllKeys, nil, -2, flagReadOnly)

	registerCommand("TTL", execTTL, readFirstKey, nil, 2, flagReadOnly)

	registerCommand("Type", execType, readFirstKey, nil, 2, flagReadOnly)

	registerCommand("Keys", execKeys, noPrepare, nil, 2, flagReadOnly)

	registerCommand("Scan", execScan, noPrepare, nil, -2, flagReadOnly)
}
